"""
Main GDB-side entry point for algorithm visualization hooks.
Load via: `source scripts/gdb_layer.py`, then `algviz-load path/to/config.yml`.
"""

from __future__ import annotations

import os
import sys
import time
from pathlib import Path
from typing import Any, Dict

import gdb  # type: ignore

# ---------------------------------------------------------------------------
# Adjust sys.path so that sibling modules (config_loader, adapters, etc.) import correctly
# ---------------------------------------------------------------------------

THIS_DIR = Path(__file__).resolve().parent
if str(THIS_DIR) not in sys.path:
    sys.path.insert(0, str(THIS_DIR))

# ---------------------------------------------------------------------------
# Local imports (resolved thanks to sys.path adjustment)
# ---------------------------------------------------------------------------

from config_loader import load_config, ConfigError
from adapters import get_adapter
from serializers import build_serializer
from pretty_printer_loader import load_pretty_printers

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def current_depth(target_frame: str) -> int:
    """Count consecutive frames with the target name to estimate recursion depth."""
    depth = 0
    frame = gdb.selected_frame()
    while frame:
        if frame.name() == target_frame:
            depth += 1
        frame = frame.older()
    return depth


# ---------------------------------------------------------------------------
# Watch Manager
# ---------------------------------------------------------------------------

class WatchManager:
    def __init__(self, config: Dict[str, Any]) -> None:
        self.config = config
        self.watched = config.get("watched", [])
        self.serializer = build_serializer(config)
        self.adapters_cache: Dict[str, Any] = {}
        self.target_frame = config.get("target_frame", None)

    def get_adapter(self, adapter_name: str):
        if adapter_name not in self.adapters_cache:
            self.adapters_cache[adapter_name] = get_adapter(adapter_name)
        return self.adapters_cache[adapter_name]

    def capture(self, event_name: str) -> None:
        variables: Dict[str, Any] = {}

        for item in self.watched:
            name = item["name"]
            adapter_name = item.get("adapter", "primitive")
            try:
                value = gdb.parse_and_eval(name)
                adapter = self.get_adapter(adapter_name)
                payload = adapter.serialize(value)
            except Exception as exc:  # noqa: BLE001
                payload = {"__error__": str(exc)}

            variables[name] = payload

        frame = gdb.selected_frame()
        location = gdb.find_pc_line(frame.pc())
        payload = {
            "timestamp": time.time(),
            "event": event_name,
            "frame": frame.name(),
            "location": {
                "symtab": location.symtab and location.symtab.filename,
                "line": location.line,
            },
            "depth": current_depth(self.target_frame) if self.target_frame else 0,
            "variables": variables,
        }
        self.serializer.emit(payload)


# ---------------------------------------------------------------------------
# Custom Breakpoint Wrapper
# ---------------------------------------------------------------------------

class ConfiguredBreakpoint(gdb.Breakpoint):
    """Breakpoint that captures watched variables and auto-continues."""

    def __init__(self, spec: str, event_name: str, manager: WatchManager):
        super().__init__(spec, internal=False)
        self.event_name = event_name
        self.manager = manager

    def stop(self):
        self.manager.capture(self.event_name)
        return False  # continue execution automatically


# ---------------------------------------------------------------------------
# GDB Command Entry
# ---------------------------------------------------------------------------

class AlgVizCommand(gdb.Command):
    """Usage: algviz-load /path/to/config.yml"""

    def __init__(self):
        super().__init__("algviz-load", gdb.COMMAND_USER)

    def invoke(self, arg: str, from_tty: bool) -> None:  # noqa: ARG002
        config_path = arg.strip()
        if not config_path:
            gdb.write("[algviz] usage: algviz-load path/to/config.yml\n", gdb.STDERR)
            return

        # Pretty printers
        if load_pretty_printers():
            gdb.write("[algviz] libstdc++ pretty-printers loaded\n")

        try:
            cfg = load_config(config_path)
        except ConfigError as exc:
            gdb.write(f"[algviz] config error: {exc}\n", gdb.STDERR)
            return

        binary = cfg.get("binary")
        if not binary:
            gdb.write("[algviz] config missing 'binary' field\n", gdb.STDERR)
            return

        if not Path(binary).exists():
            gdb.write(f"[algviz] binary not found: {binary}\n", gdb.STDERR)
            return

        gdb.execute(f"file {binary}", to_string=False)

        manager = WatchManager(cfg)

        for bp in cfg.get("breakpoints", []):
            location = bp["location"]
            event_name = bp.get("event", location)
            ConfiguredBreakpoint(location, event_name, manager)
            gdb.write(f"[algviz] breakpoint set: {location} -> {event_name}\n")

        entry = cfg.get("entry")
        if entry:
            gdb.write(f"[algviz] running program (entry: {entry})\n")
            # optional: run from main or a specific entry point
            gdb.execute("run", to_string=False)
        else:
            gdb.write("[algviz] entry not specified; waiting for manual `run`\n")


# Register the command when module is imported
AlgVizCommand()